6. Rendering Multiple Styles
So, we now have a tool for
drawing text, but we still haven't reached the core of what's
interesting about Core Text: rendering multiple styles. At this point,
lacking a standard GUI widget that gives us anything like a WYSIWYG
display while editing the text, there's no really nice way to enter rich
text as you can in a word processor, with buttons to change fonts or
set colors.
Fortunately for me, I know
that you're a computer programmer, and chances are you're already
familiar with a way of marking text attributes that isn't as nice, but is
applicable to a wide range of problems: HTML! Let's extend our
text-rendering algorithm to include a very basic parsing of the text
that the user enters, looking for embedded tags that we can use to
assign attributes to the text.
I'm going to show you a very simple approach that uses an NSScanner object to scan through the entire text string, searching for just a single kind of tag: <font>
(and its matching end tag). It will use the specified values to add
attributes to the text. What we're doing here is just barely what I
would call "parsing," and will probably make you cringe if your computer
science education is less rusty than mine. I'm also well aware that the
font tag has been deprecated for
years, but it's sure an easy way to do quick-'n-dirty markup compared to
using CSS! And it works well for our purposes here.
Edit the beginning of the draw method of TextDrawingInfo as shown here, removing the crossed-out lines and replacing them with the bold line:
- (void)draw {
CGContextRef context = UIGraphicsGetCurrentContext();
//NSMutableAttributedString *attrString = [[[NSMutableAttributedString alloc] initWithString:self.text] autorelease];
//[attrString addAttribute:(NSString *)(kCTForegroundColorAttributeName)
value:(id)self.strokeColor.CGColor range:NSMakeRange(0, [self.text length])];
NSAttributedString *attrString = [self attributedStringFromMarkup:self.text];
CTFramesetterRef framesetter =
CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
Now we need to define the attributedStringFromMarkup: method in the same class (anywhere above the draw method should be fine). This uses NSScanner to look for the tags it knows about, and makes one big NSMutableAttributedString out of a number of smaller NSAttributedStrings generated between tags. Here, you also see a little usage of CTFontRef, which is Core Text's own way of referring to fonts.
- (NSAttributedString *)attributedStringFromMarkup:(NSString *)markup {
NSMutableAttributedString *attrString =
[[[NSMutableAttributedString alloc] initWithString:@""] autorelease];
NSString *nextTextChunk = nil;
NSScanner *markupScanner = [NSScanner scannerWithString:markup];
CGFloat fontSize = 0.0;
NSString *fontFace = nil;
UIColor *fontColor = nil;
while (![markupScanner isAtEnd]) {
[markupScanner scanUpToString:@"<" intoString:&nextTextChunk];
[markupScanner scanString:@"<" intoString:NULL];
if ([nextTextChunk length] > 0) {
CTFontRef currentFont =
CTFontCreateWithName((CFStringRef)(fontFace ? fontFace : self.font.fontName),
(fontSize != 0.0 ? fontSize : self.font.pointSize),
NULL);
UIColor *color = fontColor ? fontColor : self.strokeColor;
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
(id)color.CGColor, kCTForegroundColorAttributeName,
(id)currentFont, kCTFontAttributeName,
nil];
NSAttributedString *newPiece = [[[NSAttributedString alloc]
initWithString:nextTextChunk attributes:attrs] autorelease];
[attrString appendAttributedString:newPiece];
CFRelease(currentFont);
}
NSString *elementData = nil;
[markupScanner scanUpToString:@">" intoString:&elementData];
[markupScanner scanString:@">" intoString:NULL];
if (elementData) {
if ([elementData length] > 3 &&
[[elementData substringToIndex:4] isEqual:@"font"]) {
fontFace = fontFaceNameFromString(elementData);
fontSize = fontSizeFromString(elementData);
fontColor = fontColorFromString(elementData);
} else if ([elementData length] > 4 &&
[[elementData substringToIndex:5] isEqual:@"/font"]) {
// reset all values
fontSize = 0.0;
fontFace = nil;
fontColor = nil;
}
}
}
return attrString;
}
This method, in turn, offloads the parsing of the font element attributes to the following three functions. Put these directly above the attributedStringFromMarkup:@implementation block is totally fine.) method. (Although it may seem wrong, putting them inside the
static NSString *fontFaceNameFromString(NSString *attrData) {
NSScanner *attributeDataScanner = [NSScanner scannerWithString:attrData];
NSString *faceName = nil;
if ([attributeDataScanner scanUpToString:@"face=\"" intoString:NULL]) {
[attributeDataScanner scanString:@"face=\"" intoString:NULL];
if ([attributeDataScanner scanUpToString:@"\"" intoString:&faceName]) {
return faceName;
}
}
return nil;
}
static CGFloat fontSizeFromString(NSString *attrData) {
NSScanner *attributeDataScanner = [NSScanner scannerWithString:attrData];
NSString *sizeString = nil;
if ([attributeDataScanner scanUpToString:@"size=\"" intoString:NULL]) {
[attributeDataScanner scanString:@"size=\"" intoString:NULL];
if ([attributeDataScanner scanUpToString:@"\"" intoString:&sizeString]) {
return [sizeString floatValue];
}
}
return 0.0;
}
static UIColor *fontColorFromString(NSString *attrData) {
return nil;
}
You'll notice that the third method, fontColorFromString(),
isn't shown in a completed form here. In the interests of time and
space, and not wandering too far afield from our main topic, let's leave
that as an exercise for the reader, shall we?
With this in place, we now
have a way to define some characteristics of the text we enter! Build
and run Dudel, and create some new objects using the Text tool to try it
out. Here are some suggestions for putting it through its paces:
Create a paragraph with some <font size="64">really big text</font> and then more normal-sized text.
Try sticking some <font face="Courier">Courier into the mix</font> to see how multiple fonts are rendered
Mix and match these however you like. Our parser is far from perfect, and throwing something like nested font tags at it will probably confuse it, but at least it's something!